package org.piax.rospubsub;

import java.io.UnsupportedEncodingException;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Arrays;

/**
 * ADB 通信路を通るメッセージ.
 * 
 * serialized format
 * |len(4, signed)|topic_len(2, signed)|topic(n, UTF-8)|data(m)|
 */
public class ADBBridgeMessage {
    private String topic = null;    // トピック名
    private byte[] data = null;     // データ部

    /**
     * コンストラクタ
     * @param topic トピック名
     * @param data データ
     */
    public ADBBridgeMessage(String topic, byte[] data) {
        if (topic == null || "".equals(topic))
            throw new IllegalArgumentException(
                    "topic should not be null or empty");
        if (data == null || "".equals(data))
            throw new IllegalArgumentException("data should not be null or empty");

        this.topic = topic;
        this.data = data;
    }

    /**
     * トピック名を取得する。
     * @return
     */
    public String getTopic() {
        return topic;
    }

    /**
     * データ部を取得する。
     * @return
     */
    public byte[] getData() {
        return data;
    }

    /**
     * このインスタンスの内容を buf に書き出す。
     * 
     * 書き出しに失敗した場合、buf の position は変化しない。
     * @param buf インスタンスの内容を書き出す ByteBuffer
     * @throws BufferOverflowException buf の空きが不足している場合
     * @throws ReadOnlyBufferException buf が読み込み専用である場合
     */
    public void serialize(ByteBuffer buf) {
        if (buf == null)
            throw new IllegalArgumentException("buf should not be null");

        int prev_pos = buf.position();      // position を保存しておく
        try {
            byte[] topic_bytes = topic.getBytes("UTF-8");
            buf.putInt(4+2+topic_bytes.length+data.length);
            buf.putShort((short) (topic_bytes.length & 0x0000ffff));
            buf.put(topic_bytes).put(data);
        } catch (BufferOverflowException e) {
            buf.position(prev_pos);     // 保存していた position を戻す
            throw e;
        } catch (UnsupportedEncodingException e) {
            // 起きないはず
        }
    }

    /**
     * stream が deserialize 可能か否かを判定する。
     * 
     * buf の position は変化しない。
     * このメソッドが true を返した場合、deserialize は成功する。
     * @param stream ADBBridgeMessage が含まれる ByteBuffer
     * @return true deserialize 可能 false deserialize 不可(データが不足)
     */
    public static boolean deserializable(ByteBuffer stream) {
        if (stream == null)
            throw new IllegalArgumentException("stream should not be null");

        int prev_pos = stream.position();      // position を保存しておく
        try {
            int len = stream.getInt();
            if (stream.remaining() + 4 < len) {
                stream.position(prev_pos);
                return false;   // stream 内のデータサイズ + 4 < メッセージサイズの場合はデータが足りない
            }
            stream.position(prev_pos);
            return true;
        } catch (BufferUnderflowException e) {
            stream.position(prev_pos);
            return false;
        }
    }

    /**
     * stream を読み込み、ADBBridgeMessage を取得する。
     * 
     * 読み込みに失敗した場合、buf の position は変化しない。
     * @param stream ADBBridgeMessage が含まれる ByteBuffer
     * @return stream から取得した ADBBridgeMessage
     * @throws BufferUnderflowException stream に十分なデータが含まれない場合
     */
    public static ADBBridgeMessage deserialize(ByteBuffer stream) {
        if (stream == null)
            throw new IllegalArgumentException("stream should not be null");

        int prev_pos = stream.position();      // position を保存しておく
        try {
            int len = stream.getInt();
            int topic_len = stream.getShort();

            byte[] topic_bytes = new byte[topic_len];
            byte[] data_bytes = new byte[len-(4+2+topic_len)];
            stream.get(topic_bytes);
            stream.get(data_bytes);
            String topic = null;
            try {
                topic = new String(topic_bytes, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                // 起きないはず
            }
            return new ADBBridgeMessage(topic, data_bytes);
        } catch (BufferUnderflowException e) {
            stream.position(prev_pos);
            throw e;
        }
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + Arrays.hashCode(data);
        result = prime * result + ((topic == null) ? 0 : topic.hashCode());
        return result;
    }

    
    @Override
    public String toString() {
        return "ADBBridgeMessage [topic=" + topic + ", data[" + data.length +"byte(s)]="
                + Arrays.toString(data) + "]";
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        ADBBridgeMessage other = (ADBBridgeMessage) obj;
        if (!Arrays.equals(data, other.data))
            return false;
        if (topic == null) {
            if (other.topic != null)
                return false;
        } else if (!topic.equals(other.topic))
            return false;
        return true;
    }
}
